package org.hl7.fhir.r5.elementmodel;

/*
  Copyright (c) 2011+, HL7, Inc.
  All rights reserved.
  
  Redistribution and use in source and binary forms, with or without modification, 
  are permitted provided that the following conditions are met:
    
   * Redistributions of source code must retain the above copyright notice, this 
     list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above copyright notice, 
     this list of conditions and the following disclaimer in the documentation 
     and/or other materials provided with the distribution.
   * Neither the name of HL7 nor the names of its contributors may be used to 
     endorse or promote products derived from this software without specific 
     prior written permission.
  
  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
  POSSIBILITY OF SUCH DAMAGE.
  
 */



import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.utilities.FileUtilities;
import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;

/**
 * This class provides special support for parsing v2 by the v2 logical model
 * For the logical model, see the FHIRPath spec
 *         
 * @author Grahame Grieve
 *
 */
@MarkedToMoveToAdjunctPackage
public class VerticalBarParser extends ParserBase {

  /**
   * Delimiters for a message. Note that the application rarely needs to concern
   * itself with this information; it mainly exists for internal use. However if
   * a message is being written to a spec that calls for non-standard delimiters,
   * the application can set them here.  
   * 
   * @author Grahame
   *
   */
  public class Delimiters {

    /**
     * Hl7 defined default delimiter for a field
     */
    public final static char DEFAULT_DELIMITER_FIELD = '|';

    /**
     * Hl7 defined default delimiter for a component
     */
    public final static char  DEFAULT_DELIMITER_COMPONENT = '^';

    /**
     * Hl7 defined default delimiter for a subcomponent
     */
    public final static char  DEFAULT_DELIMITER_SUBCOMPONENT = '&';

    /**
     * Hl7 defined default delimiter for a repeat
     */
    public final static char  DEFAULT_DELIMITER_REPETITION = '~';

    /**
     * Hl7 defined default delimiter for an escape
     */
    public final static char  DEFAULT_CHARACTER_ESCAPE = '\\';


    /**
     * defined escape character for this message
     */
    private char escapeCharacter;

    /**
     * defined repetition character for this message
     */
      private char repetitionDelimiter;

    /**
     * defined field character for this message
     */
      private char fieldDelimiter;

    /**
     * defined subComponent character for this message
     */
      private char subComponentDelimiter;

    /**
     * defined component character for this message
     */
      private char componentDelimiter;

      /**
       * create
       *
       */
    public Delimiters() {
      super();
      reset();
    }

    public boolean matches(Delimiters other) {
      return escapeCharacter == other.escapeCharacter &&
      repetitionDelimiter == other.repetitionDelimiter &&
      fieldDelimiter == other.fieldDelimiter &&
      subComponentDelimiter == other.subComponentDelimiter &&
      componentDelimiter == other.componentDelimiter;
    }
    
    /**
     * get defined component character for this message
     * @return
     */
    public char getComponentDelimiter() {
      return componentDelimiter;
    }

    /**
     * set defined component character for this message
     * @param componentDelimiter
     */
    public void setComponentDelimiter(char componentDelimiter) {
      this.componentDelimiter = componentDelimiter;
    }

    /**
     * get defined escape character for this message
     * @return
     */
    public char getEscapeCharacter() {
      return escapeCharacter;
    }

    /**
     * set defined escape character for this message
     * @param escapeCharacter
     */
    public void setEscapeCharacter(char escapeCharacter) {
      this.escapeCharacter = escapeCharacter;
    }

    /**
     * get defined field character for this message
     * @return
     */
    public char getFieldDelimiter() {
      return fieldDelimiter;
    }

    /**
     * set defined field character for this message
     * @param fieldDelimiter
     */
    public void setFieldDelimiter(char fieldDelimiter) {
      this.fieldDelimiter = fieldDelimiter;
    }

    /**
     * get repeat field character for this message
     * @return
     */
    public char getRepetitionDelimiter() {
      return repetitionDelimiter;
    }

    /**
     * set repeat field character for this message
     * @param repetitionDelimiter
     */
    public void setRepetitionDelimiter(char repetitionDelimiter) {
      this.repetitionDelimiter = repetitionDelimiter;
    }

    /**
     * get sub-component field character for this message
     * @return
     */
    public char getSubComponentDelimiter() {
      return subComponentDelimiter;
    }

    /**
     * set sub-component field character for this message
     * @param subComponentDelimiter
     */
    public void setSubComponentDelimiter(char subComponentDelimiter) {
      this.subComponentDelimiter = subComponentDelimiter;
    }

    /**
     * reset to default HL7 values
     *
     */
    public void reset () {
      fieldDelimiter = DEFAULT_DELIMITER_FIELD;
      componentDelimiter = DEFAULT_DELIMITER_COMPONENT;
      subComponentDelimiter = DEFAULT_DELIMITER_SUBCOMPONENT;
      repetitionDelimiter = DEFAULT_DELIMITER_REPETITION;
      escapeCharacter = DEFAULT_CHARACTER_ESCAPE;
    }
    
    /**
     * check that the delimiters are valid
     * 
     * @throws FHIRException
     */
    public void check() throws FHIRException {
        rule(componentDelimiter != fieldDelimiter, "Delimiter Error: \""+componentDelimiter+"\" is used for both CPComponent and CPField");
        rule(subComponentDelimiter != fieldDelimiter, "Delimiter Error: \""+subComponentDelimiter+"\" is used for both CPSubComponent and CPField");
        rule(subComponentDelimiter != componentDelimiter, "Delimiter Error: \""+subComponentDelimiter+"\" is used for both CPSubComponent and CPComponent");
        rule(repetitionDelimiter != fieldDelimiter, "Delimiter Error: \""+repetitionDelimiter+"\" is used for both Repetition and CPField");
        rule(repetitionDelimiter != componentDelimiter, "Delimiter Error: \""+repetitionDelimiter+"\" is used for both Repetition and CPComponent");
        rule(repetitionDelimiter != subComponentDelimiter, "Delimiter Error: \""+repetitionDelimiter+"\" is used for both Repetition and CPSubComponent");
        rule(escapeCharacter != fieldDelimiter, "Delimiter Error: \""+escapeCharacter+"\" is used for both Escape and CPField");
        rule(escapeCharacter != componentDelimiter, "Delimiter Error: \""+escapeCharacter+"\" is used for both Escape and CPComponent");
        rule(escapeCharacter != subComponentDelimiter, "Delimiter Error: \""+escapeCharacter+"\" is used for both Escape and CPSubComponent");
        rule(escapeCharacter != repetitionDelimiter, "Delimiter Error: \""+escapeCharacter+"\" is used for both Escape and Repetition");
    }

    /**
     * check to see whether ch is a delimiter character (vertical bar parser support)
     * @param ch
     * @return
     */
    public boolean isDelimiter(char ch) {
      return ch == escapeCharacter || ch == repetitionDelimiter || ch == fieldDelimiter || ch == subComponentDelimiter  || ch ==  componentDelimiter;
    }

    /** 
     * check to see whether ch is a cell delimiter char (vertical bar parser support)
     * @param ch
     * @return
     */
    public boolean isCellDelimiter(char ch) {
      return ch == repetitionDelimiter || ch == fieldDelimiter || ch == subComponentDelimiter  || ch ==  componentDelimiter;
    }

    /**
     * get the escape for a character
     * @param ch
     * @return
     */ 
    public String getEscape(char ch) {
      if (ch == escapeCharacter)
        return escapeCharacter + "E" + escapeCharacter;
      else if (ch == fieldDelimiter)
        return escapeCharacter + "F" + escapeCharacter;
      else if (ch == componentDelimiter)
        return escapeCharacter + "S" + escapeCharacter;
      else if (ch == subComponentDelimiter)
        return escapeCharacter + "T" + escapeCharacter;
      else if (ch == repetitionDelimiter)
        return escapeCharacter + "R" + escapeCharacter;
      else
        return null;
      }

    /**
     * build the MSH-2 content
     * @return
     */
    public String forMSH2() {
      return "" + componentDelimiter + repetitionDelimiter + escapeCharacter + subComponentDelimiter;
    }

    /** 
     * check to see whether ch represents a delimiter escape
     * @param ch
     * @return
     */
    public boolean isDelimiterEscape(char ch) {
      return ch == 'F' || ch == 'S'  || ch == 'E' || ch == 'T' || ch == 'R';
    }

    /** 
     * get escape for ch in an escape
     * @param ch
     * @return
     * @throws DefinitionException 
     * @throws FHIRException
     */
    public char getDelimiterEscapeChar(char ch) throws DefinitionException {
      if (ch == 'E')
        return escapeCharacter;
      else if (ch == 'F')
        return fieldDelimiter;
      else if (ch == 'S')
        return componentDelimiter;
      else if (ch == 'T')
        return subComponentDelimiter;
      else if (ch == 'R')
        return repetitionDelimiter;
      else
        throw new DefinitionException("internal error in getDelimiterEscapeChar");
    }
  }
  
  public class VerticalBarParserReader {

    
    private BufferedInputStream stream;
    private String charsetName; 
    private InputStreamReader reader = null;
    private boolean finished;
    private char peeked;
    private char lastValue;
    private int offset;
    private int lineNumber;

    public VerticalBarParserReader(BufferedInputStream stream, String charsetName) throws FHIRException {
      super();
      setStream(stream);
      setCharsetName(charsetName);
      open();
    }

    public String getCharsetName() {
      return charsetName;
    }

    public void setCharsetName(String charsetName) {
      this.charsetName = charsetName;
    }

    public BufferedInputStream getStream() {
      return stream;
    }

    public void setStream(BufferedInputStream stream) {
      this.stream = stream;
    }
    
    private void open() throws FHIRException {
      try {
        stream.mark(2048);
        reader = new InputStreamReader(stream, charsetName);
        offset = 0;
        lineNumber = 0;
        lastValue = ' ';
        next();
      } catch (Exception e) {
        throw new FHIRException(e);
      }
    }

    private void next() throws IOException, FHIRException {
      finished = !reader.ready();
      if (!finished) {
        char[] temp = new char[1];
        rule(reader.read(temp, 0, 1) == 1, "unable to read 1 character from the stream");
        peeked = temp[0];
      } 
    }

    public String read(int charCount) throws FHIRException {
      String value = "";
      for (int i = 0; i < charCount; i++) 
        value = value + read();
      return value;
    }
    
    public void skipEOL () throws FHIRException {
      while (!finished && (peek() == '\r' || peek() == '\n'))
        read();
    }
    
    public char read () throws FHIRException {
      rule(!finished, "No more content to read");
      char value = peek();
      offset++;
      if (value == '\r' || value == '\n') {
        if (lastValue != '\r' || value != '\n')
          lineNumber++;
      }
      lastValue = value;
      try {
        next();
      } catch (Exception e) {
        throw new FHIRException(e);
      } 
      return value;
    }

    public boolean isFinished () {
      return finished;    
    }
    
    public char peek() throws FHIRException {
      rule(!finished, "Cannot peek");
      return peeked;    
    }

    public void mark() {
      stream.mark(2048);
    }
    
    public void reset() throws FHIRException {
      try {
        stream.reset();
      } catch (IOException e) {
        throw new FHIRException(e);
      }
      open();
    }

    public boolean IsEOL() throws FHIRException {
      return peek() == '\r' || peek() == '\n';
    }

    public int getLineNumber() {
      return lineNumber;
    }

    public int getOffset() {
      return offset;
    }

  }

  public VerticalBarParser(IWorkerContext context) {
    super(context);
  }

  private String charset = "ASCII";
  private Delimiters delimiters = new Delimiters();
  
  @Override
  public List<ValidatedFragment> parse(InputStream inStream) throws IOException, FHIRFormatError, DefinitionException, FHIRException {
    StructureDefinition sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/v2/StructureDefinition/Message");
    Element message = new Element("Message", new Property(context, sd.getSnapshot().getElementFirstRep(), sd, getProfileUtilities(), getContextUtilities()));
    byte[] content = FileUtilities.streamToBytes(inStream);
    ByteArrayInputStream stream = new ByteArrayInputStream(content);
    VerticalBarParserReader reader = new VerticalBarParserReader(new BufferedInputStream(stream), charset);
    
    preDecode(reader);
    while (!reader.isFinished()) //  && (getOptions().getSegmentLimit() == 0 || getOptions().getSegmentLimit() > message.getSegments().size()))
      readSegment(message, reader);
    List<ValidatedFragment> res = new ArrayList<>();
    res.add(new ValidatedFragment(ValidatedFragment.FOCUS_NAME, "hl7", message, content, false));
    return res;
  }

  private void preDecode(VerticalBarParserReader reader) throws FHIRException {
    reader.skipEOL();
    String temp = reader.read(3);
    rule(temp.equals("MSH") || temp.equals("FHS"), "Found '" + temp + "' looking for 'MSH' or 'FHS'");
    readDelimiters(reader);
    // readVersion(message); - probably don't need to do that? 
    // readCharacterSet();
    reader.reset(); // ready to read message now
  }

  private void rule(boolean test, String msg) throws FHIRException {
    if (!test)
      throw new FHIRException(msg);    
  }

  private void readDelimiters(VerticalBarParserReader reader) throws FHIRException {
    delimiters.setFieldDelimiter(reader.read());
    if (!(reader.peek() == delimiters.getFieldDelimiter()))
      delimiters.setComponentDelimiter(reader.read());
    if (!(reader.peek() == delimiters.getFieldDelimiter()))
      delimiters.setRepetitionDelimiter(reader.read());
    if (!(reader.peek() == delimiters.getFieldDelimiter()))
      delimiters.setEscapeCharacter(reader.read());
    if (!(reader.peek() == delimiters.getFieldDelimiter()))
      delimiters.setSubComponentDelimiter(reader.read());
    delimiters.check();
  }

  private void readSegment(Element message, VerticalBarParserReader reader) throws FHIRException {
    Element segment = new Element("segment", message.getProperty().getChild("segment"));
    message.getChildren().add(segment);
    Element segmentCode = new Element("code", segment.getProperty().getChild("code"));
    segment.getChildren().add(segmentCode);
    segmentCode.setValue(reader.read(3));
    
    int index = 0;
    while (!reader.isFinished() && !reader.IsEOL()) {
      index++;
      readField(reader, segment, index);
      if (!reader.isFinished() && !reader.IsEOL())
        rule(reader.read() == delimiters.getFieldDelimiter(), "Expected to find field delimiter");
    }
    if (!reader.isFinished())
      reader.skipEOL();    
  }


  
  private void readField(VerticalBarParserReader reader, Element segment, int index) {
    // TODO Auto-generated method stub
    
  }

  @Override
  public void compose(Element e, OutputStream destination, OutputStyle style, String base) {
    // TODO Auto-generated method stub

  }

}